Conversation
| @@ -0,0 +1,63 @@ | |||
| ## ステップ1 | |||
| 制約にO(log n)とあったので思いついたのは、バイナリーサーチツリーを用いた | |||
| 探索した結果端に辿り着いた場合の処理に時間がかかった | |||
There was a problem hiding this comment.
二分探索についてはDiscord内で沢山議論がされているので、探してみると良さそうです。
自分が役に立ったなと思うのはこの辺
Yoshiki-Iwasa/Arai60#35 (comment)
Yoshiki-Iwasa/Arai60#35 (comment)
https://discord.com/channels/1084280443945353267/1084283898617417748/1282392271643345007
(上記のリンク先でOdaさんやnodchipさんが話してる内容)
There was a problem hiding this comment.
seal-azarashi/leetcode#39 (comment)
こういう風にコードを変えたときにどこは動いてどこは動かないかを分かっていれば、正しく頭の中でモデルが作れていると思います。
There was a problem hiding this comment.
@Yoshiki-Iwasa @oda
資料ありがとうございます。同ジャンルの問題を解いてみてこの辺よくわかっていないと気づいたのでじっくり落とし込んでみます。
| } | ||
|
|
||
| private: | ||
| int SearchInsertIndex(int start, int end, vector<int>& nums, int target) { |
There was a problem hiding this comment.
これ、start と end はどういう変数であるかを意識していたらいいと思います。
「nums の start よりも左の要素はすべて target 未満」ということでしょうか。
| return middle; | ||
| } | ||
| if (nums[middle] < target) { | ||
| start++; |
There was a problem hiding this comment.
こうすると、target が大きい場合、start が一つずつ増えていくので、全部舐めることになりますね。
There was a problem hiding this comment.
@oda
binary searchになっておりませんでした。他の数問といて戻ってくるとおかしいですね。
start = middle + 1としました。
| if (start > end) { | ||
| return start; | ||
| } | ||
| int middle = (start + end) / 2; |
There was a problem hiding this comment.
今回の問題の制約では起こりえないのですが、値が大きい場合のオーバーフローを避けるため、
int middle = start + (end - start) / 2;
と書くことをお勧めいたします。
There was a problem hiding this comment.
leetcodeにも開設ございました。
ありがとうございます。
35.SearchInsertPosition/memo.md
Outdated
| @@ -0,0 +1,63 @@ | |||
| ## ステップ1 | |||
| 制約にO(log n)とあったので思いついたのは、バイナリーサーチツリーを用いた | |||
There was a problem hiding this comment.
回答ソースコードで使用しているのはバイナリーサーチツリーではなくバイナリーサーチではないでしょうか?
| private: | ||
| int SearchInsertIndex(int start, int end, vector<int>& nums, int target) { | ||
| // 端にいってしまった場合の処理 | ||
| if (start > end) { |
There was a problem hiding this comment.
区間を閉区間としてとらえているため、 start > end となることはないと思います。終了条件は start == end となると思います。
There was a problem hiding this comment.
There was a problem hiding this comment.
1.問題のモデル化
の部分で、
- 一番左端の true のいちを求める、もしくは false と true の問題とみなす。
- 上記問題を解くにあたり、対象の位置を含む区間を定義する。
が抜けているように思いました。
4.ループ不変条件の設定
一番書くべきことは
- ループの不変条件を start < end とする。
だと思います。
5.探索ロジックの設計
nums[mid] < targetがtrueなら、startをmiddle + 1に更新
この書き方ですと、 start を mid に設定してはいけない理由が分かりませんでした。
nums[mid] < target より、探したい対象が mid より右にあることが分かります。区間は閉区間で、区間の中に対象が含まれるので、 start は mid より右に設定してあげるのがよいはずです。そのため、 start を mid + 1 に設定します。
また、 start を mid に設定した場合、区間内の要素数が残り 2 個になったときに、無限ループとなります。
nums[mid] >= targetがtrueなら、endをmiddleに更新
endをmiddle - 1に更新してしまうと、start > endが発生しうる
最終的な挙動からボトムアップに思考しているように見え、違和感を感じました。二分探索の問題のモデルからトップダウンに考えていき、その内容を記述するのが良いと思います。
nums[mid] >= target の場合、 mid の位置に対象がある場合があるため、区間を狭めつつ mid を区間内に含めるため、 end = mid とすると考えるのが良いと思います。
There was a problem hiding this comment.
@nodchip
詳細に説明いただきありがとうございます。
一番書くべきことは
ループの不変条件を start < end とする。
ここですが start <= middle < end のような書き方でもよろしいでしょうか?
startとendの関係は、start < end変わらないとは思いますが、middleがある方が理解しやすいと思いました。
最終的な挙動からボトムアップに思考しているように見え、違和感を感じました。二分探索の問題のモデルからトップダウンに考えていき、その内容を記述するのが良いと思います。
指摘されるまで気づかなかったのですが、確かに答えから説明を作っていました。
頂いた指摘事項を元にstep4の説明を修正しました。また練習として半開区間を用いたstep5.cppを追加しました🙇♂️
お手隙の際に見ていただけると幸いです。
f1f955b
There was a problem hiding this comment.
ここですが start <= middle < end のような書き方でもよろしいでしょうか?
閉区間で考えているため、 middle == end の場合もあります。そのため、 middle < end とはかけないと思います。 start < end、start <= middle、middle <= end と書くしかないように思います。
| 7.実行 | ||
| leet codeにて動作確認 | ||
|
|
||
| */ |
There was a problem hiding this comment.
なんとなく理解しているか不安を感じています。
「この関数の仕事を手作業でやっているとしましょう。シフト制で SearchInsertIndex の呼び出しが起きるごとに、人が交代します。
あなたは、当番で SearchInsertIndex の呼び出しがおきたという連絡を受けて、仕事につきます。
start, end, nums, target が与えられました。
ここまで働いている人たちがきちんと仕事をしていたら、start, end, nums, target についてどのようなことがいえますか。」
という質問に答えられますか。
まず、自分が呼び出した前任者がしていた仕事は3通りの可能性がありますね。
SearchInsertIndex(0, nums.size() - 1, nums, target);
SearchInsertIndex(middle + 1, end, nums, target);
SearchInsertIndex(start, middle, nums, target);前任者たちが正しく仕事をしていたら、どういう条件のものが送られてきますか。
There was a problem hiding this comment.
なんとなく理解しているか不安を感じています。
紙に書いて処理を追ったり、なんとか言葉に落とし込んだりしているもののまだスッキリしておりません。
手作業だと下記のような感じでしょうか。
1番最初に作業をする人からは、確認作業の範囲と確認対象とターゲットの数字が引き継がれます。
SearchInsertIndex(0, nums.size() - 1, nums, target);
作業は始まったところであるため、範囲はnumsの先頭から1番最後まで取ります。
2番目作業者からは効率よく探索を行うためnums内のおおよそどこにターゲットがあるのかあたり付けをします。
startとendの情報を元に、numsの真ん中の数字を確認します。
この時にターゲットが真ん中より大きい場合は、下記の形で呼び出します。
SearchInsertIndex(middle + 1, end, nums, target);
次の作業者へ真ん中の数字より大きいためstart 〜 middle内にはターゲットが存在しないことを伝えます。
次の作業者の作業範囲はmiddleの次の位置から、numsの最後となります。
ターゲットが真ん中の数字以下の場合(真ん中より大きくない場合)は下記の形で呼び出します。
SearchInsertIndex(start, middle, nums, target);
これはstartからmiddleを含むどこかにターゲットが存在することを知らせております。
numsの始まりから真ん中を含む範囲を確認するよう依頼します。
3番目以降の作業者も効率よく作業をするため、前任から引き継がれた作業範囲を元に
真ん中を見つけて次の人へ作業範囲を分割して引き継ぎます。
これを作業範囲がなくなるまで繰り返します。
start, end, nums, target についてどのようなことがいえますか。
・startはtargetの位置に対して常に左側(startの位置とtargetの位置がイコールの場合もある)
・endはtargetの位置に対して常に右側
・前任者から引き継いだstartとendは、自分がmiddle分割したmiddle含むまでの範囲とmiddleの次以降から最後までを合わせたものと一致します。
2番目の作業者が分割したstart 〜 middleとmiddleの次 〜 end くっつけたものは、1番最初に作業をした人が調べたstart 〜 endと同じになります。
・nums, target は常に不変です。
There was a problem hiding this comment.
ありがとうございます。そうですね。正しい方向に考えていると思います。
endはtargetの位置に対して常に右側
これなんですが、一番はじめのループのときだけ、これが成立していませんよね。
end = nums.size() - 1 としているので、同じ可能性があります。
これが最後に if で分岐をする羽目になった理由です。
|
step4に修正反映もれございました🙇♂️ |
35.SearchInsertPosition/step4.cpp
Outdated
| @@ -0,0 +1,61 @@ | |||
| /* | |||
| 1.問題のモデル化 | |||
| ターゲットより大きいのか、以下なのか2つ以上の相補的な状態に分類することができるのでfalseとtrueの問題とみなす。 | |||
There was a problem hiding this comment.
falseとtrueの問題とみなす。
この部分の意味が理解できませんでした。この部分を文字通り読むと、 true または false を返す問題のように思えます。元の問題は、nums の中に target が存在していればその位置を、なければ挿入位置を返す問題です。
また、二分探索の問題としてとらえるにあたり、 [false, false, ..., false, true, true, ..., true] といった配列の一番左の true の位置、または false と true の境界の位置を探す、という記述が必要だと思います。
仮に自分が書くとするならば、以下のように書くと思います。
nums の各要素について、 target 未満を false、 target 以上を true とするような配列を作る。この配列は [false, false, ..., false, true, true, ..., true] のように、 0 個以上の false が並び、そのあと 0 個以上の true が並ぶ。この配列の配列の中で、一番左の true の位置を探す。
35.SearchInsertPosition/step4.cpp
Outdated
| 今回は閉区間として探索を行う。 | ||
|
|
||
| 3.初期値の設定 | ||
| startを0、endを配列の最後の要素nums.size() - 1として探索する。 |
There was a problem hiding this comment.
この書き方ですと、初期値の設定のフェーズにもかかわらず、探索を始めてしまっているように見えます。
startを0、endを配列の最後の要素nums.size() - 1とする。
でよいと思います。
35.SearchInsertPosition/step4.cpp
Outdated
|
|
||
| 3.初期値の設定 | ||
| startを0、endを配列の最後の要素nums.size() - 1として探索する。 | ||
| 配列の全要素を探索するイメージ。 |
There was a problem hiding this comment.
こちらも、初期値の設定のフェーズにもかかわらず、探索を始めてしまっているように見えます。さらに、この書き方ですと、全要素を探索すると時間がかかるので、一部のみ調べて、処理時間を節約するという点がポイントにもかかわらず、全要素を調べようとしているかのように感じられます。
35.SearchInsertPosition/step4.cpp
Outdated
| 配列の全要素を探索するイメージ。 | ||
|
|
||
| 4.ループ不変条件の設定 | ||
| startとendの真ん中をmiddleとして、ループの普遍条件は |
35.SearchInsertPosition/step4.cpp
Outdated
| ・start <= middle < end | ||
|
|
||
| 5.探索ロジックの設計 | ||
| nums[middle] < targetがtrueであれば、middleより左側にtargetは存在しないので |
There was a problem hiding this comment.
middle の位置にも target は存在していないので、この記述ですと誤りだと思います。また、「nums[middle] < targetがtrueであれば」表現が重複しているように思います。「nums[middle] < target の場合」でよいと思います。
nums[middle] < target の場合、middleおよびその左側にtargetは存在しないので
35.SearchInsertPosition/step4.cpp
Outdated
|
|
||
| 5.探索ロジックの設計 | ||
| nums[middle] < targetがtrueであれば、middleより左側にtargetは存在しないので | ||
| startをmiddle + 1に更新 |
There was a problem hiding this comment.
読み手の認知負荷を下げるため、同一文章の中で、体言止めと用言止めは統一することをお勧めいたします。
start を middle + 1 に更新する。
There was a problem hiding this comment.
|
Binary search学習教材メモ |
問題へのリンク
https://leetcode.com/problems/search-insert-position/description/
問題文(プレミアムの場合)
備考
次に解く問題の予告
Find Minimum in Rotated Sorted Array
フォルダ構成
LeetCodeの問題ごとにフォルダを作成します。
フォルダ内は、step1.cpp、step2.cpp、step3.cpp、for.cppとwhile.cppとmemo.mdとなります。
memo.md内に各ステップで感じたことを追記します。